﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Reflection;
using stunnware.CRM.Fetch;

namespace LinqtoCRM
{
    internal class WhereBuilder : ExpressionVisitor
    {
        private FetchStatement stmt;
        private Expression _e;

        public WhereBuilder(FetchStatement stmt, Expression e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            this.stmt = stmt;
            this._e = e;
        }

        private class FetchFilters
        {
            private Dictionary<FetchEntityBase, FetchFilter> _filters = new Dictionary<FetchEntityBase, FetchFilter>();

            public FetchFilters(FetchCondition cond, FetchEntityBase entity)
            {
                FetchFilter filter = new FetchFilter(FetchFilterType.And);
                filter.Conditions.Add(cond);
                _filters[entity] = filter;
            }

            public FetchFilters(FetchStatement stmt)
            {
                foreach(FetchEntity e in stmt.GetEntities().Where(e => e.Filter != null))
                {
                    _filters[e] = e.Filter;
                }
            }

            private static void Append(FetchFilter left, FetchFilter right)
            {
                foreach (FetchCondition cond in right.Conditions)
                    left.Conditions.Add(cond);
                foreach (FetchFilter filter in right.Filters)
                    left.Filters.Add(filter);
            }

            public static FetchFilters operator |(FetchFilters l, FetchFilters r)
            {
                if (l._filters.Keys.Count > 1 || r._filters.Keys.Count > 1)
                {
                    throw new Exception("FetchXml doesn't support or across entities");
                }

                FetchEntityBase entity = l._filters.First().Key;

                if (entity != r._filters.First().Key)
                {
                    throw new Exception("Entities must be the same in order to use or");
                }

                FetchFilter left = l._filters.First().Value;
                FetchFilter right = r._filters.First().Value;

                if (left.SingleCondition())
                {
                    left.FilterType = FetchFilterType.Or;
                }

                // try to make the hierarchy as flat as possible
                if (left.FilterType == FetchFilterType.Or)
                {
                    if (right.FilterType == FetchFilterType.Or || right.SingleCondition())
                        Append(left, right);
                    else
                        left.Filters.Add(right);
                }
                else
                {
                    FetchFilter or = new FetchFilter(FetchFilterType.Or);
                    or.Filters.Add(left);
                    or.Filters.Add(right);

                    l._filters[entity] = or;
                }

                return l;
            }

            private FetchFilter this[FetchEntityBase e]
            {
                get
                {
                    return _filters[e];
                }
            }

            public static FetchFilters operator &(FetchFilters l, FetchFilters r)
            {
                foreach (KeyValuePair<FetchEntityBase, FetchFilter> kv in r._filters)
                {
                    var andFilter = new FetchFilter(FetchFilterType.And);
                    if (l._filters.ContainsKey(kv.Key))
                    {
                        var filter = l._filters[kv.Key];

                        if (filter.FilterType == FetchFilterType.And && filter.FilterType == kv.Value.FilterType)
                        {
                            foreach (FetchCondition cond in kv.Value.Conditions)
                            {
                                filter.Conditions.Add(cond);
                            }
                            foreach (FetchFilter f in kv.Value.Filters)
                            {
                                filter.Filters.Add(f);
                            }
                            andFilter = filter;
                        }
                        else
                        {
                            andFilter.Filters.Add(filter);
                            andFilter.Filters.Add(kv.Value);
                        }
                        l._filters[kv.Key] = andFilter;
                    }
                    else
                    {   
                        l._filters.Add(kv.Key,kv.Value);
                    }
                }

                return l;
            }

            public void Apply(FetchStatement stmt)
            {
                FetchFilters helper = new FetchFilters(stmt);
                helper &= this;
                foreach (KeyValuePair<FetchEntityBase, FetchFilter> kv in helper._filters)
                {
                    stmt.GetEntity(kv.Key.EntityName).Filter = kv.Value;
                }
            }
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            FetchFilters filters;
            Expression e = VisitMethodCall(m, out filters);
            if (filters != null)
            {
                filters.Apply(this.stmt);
            }
            return e;
        }

        private Expression VisitMethodCall(MethodCallExpression m, out FetchFilters filters)
        {
            filters = null;
            if (m.Method.DeclaringType == typeof(String))
            {
                this.VisitStringOperation(m, out filters);
            }
            else if (m.Method.Name == "Contains" && m.Method.DeclaringType.Name == "List`1")
            {
                this.VisitListContains(m, out filters);
            }
            else
            {
                throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
            }

            return m;
        }

        private FetchFilters HandleComparison(BinaryExpression b)
        {
            FetchFilters result = null;
            Action<FetchConditionOperator> handleComparison = (oper) =>
            {
                result = CreateFetchFilters(b, oper);
            };
            switch (b.NodeType)
            {
                case ExpressionType.Equal:
                    handleComparison(FetchConditionOperator.Equal);
                    break;
                case ExpressionType.NotEqual:
                    handleComparison(FetchConditionOperator.NotEqual);
                    break;
                case ExpressionType.LessThan:
                    handleComparison(FetchConditionOperator.LowerThan);
                    break;
                case ExpressionType.LessThanOrEqual:
                    handleComparison(FetchConditionOperator.LowerEqual);
                    break;
                case ExpressionType.GreaterThan:
                    handleComparison(FetchConditionOperator.GreaterThan);
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    handleComparison(FetchConditionOperator.GreaterEqual);
                    break;
                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
            }
            return result;
        }

        private FetchFilters VisitBinaryPart(Expression e)
        {
            if (e is BinaryExpression)
            {
                return VisitBinaryRecursive(e as BinaryExpression);
            }
            else if (e is MethodCallExpression)
            {
                FetchFilters filters;
                VisitMethodCall(e as MethodCallExpression, out filters);
                return filters;
            }
            throw new Exception("Only BinaryExpressions and MethodCallExpressions handled as BinaryExpressions sides");
        }

        private FetchFilters VisitBinaryRecursive(BinaryExpression b)
        {
            FetchFilters left, right;
            switch (b.NodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    left = this.VisitBinaryPart(b.Left);
                    right = this.VisitBinaryPart(b.Right);
                    return left & right;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    left = this.VisitBinaryPart(b.Left);
                    right = this.VisitBinaryPart(b.Right);
                    return left | right;
                default:
                    return HandleComparison(b);
            }
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            FetchFilters filters = VisitBinaryRecursive(b);
            filters.Apply(this.stmt);
            return b;
        }

        private FetchFilters CreateFetchFilters(BinaryExpression b, FetchConditionOperator oper)
        {
            //TODO: unify this and SetConditionalValues
            MemberExpression left = ((b.Left is UnaryExpression) ? (b.Left as UnaryExpression).Operand : b.Left) as MemberExpression;
            if (left != null)
            {
                FetchCondition cond = new FetchCondition();
                cond.ConditionOperator = oper;
                SetConditionalValues(cond, b);

                return new FetchFilters(cond, GetFetchEntity(left));
            }
            else
            {
                throw new Exception(string.Format("Only MemberExpression supported as Left part for operator {0} at the moment", oper));
            }
        }

        private void VisitMethodCall(MethodCallExpression methodCallExpression, FetchFilter filter)
        {
            string l = (string)Expression.Lambda<Func<String>>(methodCallExpression.Arguments[0]).Compile().DynamicInvoke();
            string property = ((System.Linq.Expressions.MemberExpression)(methodCallExpression.Object)).Member.Name;
            FetchCondition cond = new FetchCondition(property);
            cond.ConditionOperator = FetchConditionOperator.Like;
            if (methodCallExpression != null)
            {
                string likeString = String.Empty;
                switch (methodCallExpression.Method.Name)
                {
                    case "Equals": likeString = l; break;
                    case "EndsWith": likeString = "%" + l; break;
                    case "StartsWith": likeString = l + "%"; break;
                    case "Contains": likeString = "%" + l + "%"; break;
                    default: throw new NotSupportedException(methodCallExpression.Method.Name);
                }
                cond.Values.Add(likeString);
                filter.Conditions.Add(cond);
            }
        }

        private void VisitStringOperation(MethodCallExpression like, out FetchFilters filters)
        {
            string l = (string)Expression.Lambda<Func<String>>(like.Arguments[0]).Compile().DynamicInvoke();

            MemberExpression mex = like.Object as MemberExpression;

            string property = ((System.Linq.Expressions.MemberExpression)(like.Object)).Member.Name;
            FetchCondition f = new FetchCondition(property);
            f.ConditionOperator = FetchConditionOperator.Like;

            string likeString = String.Empty;
            switch (like.Method.Name)
            {
                case "Equals": likeString = l; break;
                case "EndsWith": likeString = "%" + l; break;
                case "StartsWith": likeString = l + "%"; break;
                case "Contains": likeString = "%" + l + "%"; break;
            }
            f.Values.Add(likeString);
            filters = new FetchFilters(f, GetFetchEntityInOperations(mex.Expression));
        }

        private void SetConditionalValues(FetchCondition cond, BinaryExpression b)
        {
            MemberExpression mexp = ((b.Left is UnaryExpression) ? (b.Left as UnaryExpression).Operand : b.Left) as MemberExpression;
            cond.Attribute = GetConditionMemberExpression(mexp).Member.Name;

            object value = Expression.Lambda(b.Right).Compile().DynamicInvoke();
            // check for null comparison
            if (value == null)
            {
                if (cond.ConditionOperator == FetchConditionOperator.Equal)
                    cond.ConditionOperator = FetchConditionOperator.Null;
                else if (cond.ConditionOperator == FetchConditionOperator.NotEqual)
                    cond.ConditionOperator = FetchConditionOperator.NotNull;
                else
                    throw new Exception("Strange null-comparison");
            }
            else
            {
                if (value is DateTime)
                {
                    // If the value to compare to is of type DateTime, format it in a way fetchXml will accept
                    cond.Values.Add(((DateTime)value).ToString("s"));
                }
                else
                {
                    cond.Values.Add(value);
                }
            }
        }

        private MemberExpression GetConditionMemberExpression(MemberExpression mexp)
        {
            if (mexp.Type.Name == "Key")
                throw new ArgumentException("Predicate on type \"Key\" invalid, please use the .Value property");
            MemberExpression innermexp = mexp.Expression as MemberExpression;

            // Changes this if, to go a level deeper anytime the member AND the parent member is a property (e.g. c.status.Value or c.createdon.UserTime)
            // Previous version only did this for Value-property
            if (mexp.Member.MemberType.Equals(MemberTypes.Property) && innermexp != null && innermexp.Member.MemberType.Equals(MemberTypes.Property) && innermexp.Type.BaseType.Name != "BusinessEntity")
            {
                // need to go one level deeper
                return innermexp;
            }

            return mexp;
        }

        private FetchEntityBase GetFetchEntity(MemberExpression mex)
        {
            do
            {
                FetchEntityBase e = GetFetchEntity(mex.Type.Name, null);
                if (e != null)
                {
                    return e;
                }
                mex = mex.Expression as MemberExpression;
            }
            while (mex != null);

            return this.stmt.Entity;
        }

        private FetchEntityBase GetFetchEntity(string entityName, FetchEntityBase defaultResult)
        {
            FetchEntityBase result = this.stmt.GetEntity(entityName);
            return result == null ? defaultResult : result;
        }

        private FetchEntityBase GetFetchEntityInOperations(Expression e)
        {
            return e is ParameterExpression ?
                 this.stmt.GetEntity((e as ParameterExpression).Type.Name) :
                 GetFetchEntity(e as MemberExpression);
        }

        /// <summary>
        /// This creates "IN" queries for contains-clauses on lists
        /// </summary>
        /// <param name="contains"></param>
        private void VisitListContains(MethodCallExpression contains, out FetchFilters filters)
        {
            var vals = Expression.Lambda(contains.Object).Compile().DynamicInvoke() as System.Collections.ICollection;
            if (vals == null)
            {
                filters = null;
                return; // TODO: consider throwing exception
            }

            MemberExpression mexp = GetConditionMemberExpression(contains.Arguments[0] as MemberExpression);

            string attribute = mexp.Member.Name;

            FetchCondition f = new FetchCondition(attribute);
            f.ConditionOperator = FetchConditionOperator.In;

            f.Values.AddRange(vals);
            filters = new FetchFilters(f, GetFetchEntityInOperations(mexp));
        }

        public Expression Process()
        {
            return base.Visit(_e);
        }
    }
}
